<?php
/**
 *	[ Devian Art Downloader]
 *
 *	Author : Sorceress Lulu
 *	  Date : 17 jan 2013
 *
 *	Simple tool to donwload images from devian art.
 *	Usage:
 *		php dup.php -q "query" -to /path/to/save
 *  Optionally:
 *      -from [page] -to [page] -offset [num] -mature [0/1]
 *
 *  Required PEAR, PEAR HTTP_Client:
 *      pear install HTTP_Client
 */

require_once( "HTTP/Client.php" );
require_once( "Net/URL.php" );

/**
 * @desc Application to run this script
 */
class DUPApplication
{
    protected $http;
    protected $searchQuery;
    protected $saveTo;
    protected $fromPage = 0;
    protected $toPage = null;
    protected $sortByTags = true;

    protected $mature = false;
    protected $offset = 24;

    /**
     * HTTP_Client
     */
    public function __construct () {
        $this->http = new HTTP_Client();
    }

    /**
     * @desc get params from CLI
     */
    public function parse () {
        global $argv;
        $flag = null;

        foreach ($argv as $n => $arg) {
            // PHP filename
            if ($n == 0) continue;

            // Arguments
            switch ($flag) {
                case null:
                    $arg = strtolower(str_replace('-', null, $arg)); // -arg, --arg, arg

                    switch ($arg) {
                        default:
                            throw new Exception('Unknown argument "'.$arg.'"');

                        case 'q':
                        case 'query':
                        case 'search':
                            $flag = 'searchQuery';
                            break;

                        case 'p':
                        case 'path':
                        case 'save':
                            $flag = 'saveTo';
                            break;

                        case 'mature':
                            $flag = 'mature';
                            break;

                        case 'from':
                            $flag = 'from';
                            break;

                        case 'to':
                            $flag = 'to';
                            break;

                        case 'sort':
                            $flag = 'sort';
                            break;

                        case 'offset':
                            $flag = 'offset';
                            break;
                    }
                    break;

                case 'searchQuery':
                    $this->setSearchQuery($arg);
                    $flag = null;
                    break;

                case 'saveTo':
                    $this->setPathToSave($arg);
                    $flag=null;
                    break;

                case 'mature':
                    $this->setMatureFilter($arg);
                    $flag=null;
                    break;

                case 'from':
                    $this->setFromPage($arg);
                    $flag=null;
                    break;

                case 'to':
                    $this->setToPage($arg);
                    $flag=null;
                    break;

                case 'offset':
                    $this->setOffset($arg);
                    $flag=null;
                    break;

                case 'sort':
                    $this->setSortByTags($arg);
                    $flag=null;
                    break;
            }
        }

        return $this;
    }

    /**
     * @desc Set search query
     */
    public function setSearchQuery ($query) {
        $query = trim ($query);

        if (!$query)
            throw new Exception('Empty search query');

        $this->searchQuery = $query;
        return true;
    }

    /**
     * @desc Set path to save images
     */

    public function setPathToSave ($path) {
        if (!file_exists($path))
            throw new Exception('Path "'.$path.'" not exists');

        if (!is_dir($path))
            throw new Exception('Save path "'.$path.'" is file, not directory');

        if (!is_writable($path))
            throw new Exception('Permision denied: "'.$path.'"');

        $this->saveTo = $path;
        return true;
    }

    /**
     * @desc From page
     */
    public function setFromPage ($page) {
        if ($page < 0 || !is_numeric($page))
            throw new Exception('-from: must be a numeric value');

        $this->fromPage = $page;
    }

    /**
     * @desc To page
     */
    public function setToPage($page) {
        if (!is_null($page))
            if ($page < 0 || !is_numeric($page) || $page < $this->fromPage)
                throw new Exception('-to: must be a numeric value');

        $this->toPage = $page;
    }

    /**
     * @desc Offset
     */
    public function setOffset ($n) {
        if ($page < 0 || !is_numeric($n))
            throw new Exception('-offset: must be a numeric value');

        $this->offset = $n;
    }

    /**
     * @desc Mature filter
     */
    public function setMatureFilter ($value) {
        switch ($value) {
            default:
                throw new Exception('-mature: on/off');

            case 'on':
            case '1':
            case 'true':
                $this->mature = true;
                break;

            case 'off':
            case '0':
            case 'false':
                $this->mature = false;
                break;
        }
    }

    /**
     * @desc Sort by tags
     */
    public function setSortByTags ($value) {
        switch ($value) {
            default:
                throw new Exception('-sort: on/off');

            case 'on':
            case '1':
            case 'true':
                $this->sortByTags = true;
                break;

            case 'off':
            case '0':
            case 'false':
                $this->sortByTags = false;
                break;
        }
    }

    /**
     * @desc Run app
     */
    public function run () {
        if (!$this->searchQuery ||
            !$this->saveTo)
        {
            echo 'Usage: php dup.php -q "query to search" -path /path/to/save';
            echo "Additionally: -from -to -sort -offset -mature";
            echo "\n";
            die();
        }

        // Download images
        if ($this->downloadAllImages())
            $this->trace("DOWNLOAD COMPLETE");

        // Everything ok
        return true;
    }

    /**
     * Trace
     */

    public function trace ($msg) {
        echo $msg."\n";
    }

    /**
     * @desc
     */

    public function downloadAllImages () {
        // GET data
        $getParams = array(
            'qh' => null,
            'section' => null,
            'global' => 1,
            'q' => $this->searchQuery
        );

        // Get search page
        $this->trace('Querying DevianArt');
        $this->http->get ('http://browse.deviantart.com/', $getParams);

        // Count of pages
        preg_match_all('/Found \<strong\>(\d+)\<\/strong\> relevant deviations/', $this->http->currentResponse()['body'], $matches);

        if (!isset($matches[1][0]))
            return false;

        $numImages = intval($matches[1][0]);

        // 0 images
        if ($numImages <= 0)
            throw new Exception ('DevianArt: Sorry, we found no relevant results.');

        // Download images, page by page
        for ($nPage = 0; $nPage < round($numImages/$this->offset); $nPage++)
        {
            $this->trace("###: Page: ".($nPage+1). " of ".round($numImages/$this->offset));
            $this->downloadImages($nPage);
        }

        // everything ok
        return true;
    }

    public function downloadImages ($page) {
        // json data
        $jsonParams = array(
            "freeform_user_input" => $this->searchQuery,
            "offset" => $this->offset*$page,
            "length" => $this->offset,
            "mature_filter" => $this->mature,
            "view_mode" => "thumb",
            "thumb_mode" => "grid",
            "is_mobile" => "0",
            "is_frontpage" => "0",
            "order" => "9"
        );

        // Get data
        $this->http->get("http://browse.deviantart.com/global/difi/", array(
            "c[]" => '"PortalCore","get_result_thumbs",["browse",'.json_encode($jsonParams).']',
            "t" => "json"
        ));

        // Page to parse
        $httpPage = $this->http->currentResponse()['body'];

        // Get imageURLs and categorys
        preg_match_all('/data-super-img=\\\"(.*?)\\\"/', $httpPage, $imageURLs);
        preg_match_all('/\<span class=\\\"category\\\"\>(.*?)\<\/span\>/', $httpPage, $imageCategories);

        if (!count($imageURLs) || !is_array($imageURLs))
            return false;

        // Download images
        $picturesCount = 0;

        foreach ($imageURLs[1] as $n => $url) {
            // Path to save
            if ($this->sortByTags) {
                // $category
                try {
                    // Category
                    preg_match_all('/\>(.*)\<\/a\>/', $imageCategories[1][$n], $matches);

                    // Unknown category
                    if (!isset($matches[1][0]))
                        throw new Exception('Unknown category');

                    // Category name
                    $category = htmlspecialchars_decode($matches[1][0]);

                    // mkdir
                    $path = trim($this->saveTo.'/'.$category);

                    // Category dir's exists?
                    if (file_exists($path) && !is_dir($path))
                        throw new Exception('Cannot create dir for category "'.$category.'" because file with this name allready exists');

                    // try to create dir
                    if (!file_exists($path) && !@mkdir($path))
                        throw new Exception('Failed to create directory with name "'.$category.'"');

                    // path to save
                    $pathToSave = $path;
                }
                catch (Exception $e)
                {
                    $this->trace("\t[!] ".$e->getMessage());
                    $category = null;
                }

                // path to save
                $pathToSave =  is_null($category) ? $this->saveTo : $path;
            }
            else $pathToSave = $this->saveTo;

            // Imagename
            $imageName = basename($url);

            if (!$imageName)
                throw new Exception("Unexpected error");

            // Donwload image
            try {
                $picturesCount++;
                $this->downloadFile($url, $imageName, $pathToSave);
                $this->trace("\t [$picturesCount] ".$imageName);
            }catch (Exception $e){
                $this->trace("\t[!] ".$e->getMessage());
            }
        }
    }

    /**
     * @desc Download file and save
     */
    public function downloadFile($url, $filename, $pathToSave) {
        // can we save here?
        if (!is_writable($pathToSave))
            throw new Exception('Path "'.$pathToSave.'" is not writable');

        // file exists?
        if (file_exists($pathToSave.'/'.$filename)) {
            $counter = 1;
            $pathinfo = pathinfo($filename);

            while (!isset($src) || file_exists($src)){
                $src = $pathToSave.'/'.$pathinfo['basename'].' - img '.$counter.'.'.$pathinfo['extension'];
                $counter++;
            }

            $savename = $src;
        }else $savename = $pathToSave.'/'.$filename;

        // get image
        if (!$this->http->get($url))
            throw new Exception("Failed to fetch image");

        // save
        if (!$file = fopen($savename, 'w'))
            throw new Exception('Failed to save to: '.$savename);

        fwrite($file, $this->http->currentResponse()['body']);
        fclose($file);

        // fine!
        return true;
    }
}

/** --
 *	Run script
 */

try
{
    $Application = new DUPApplication();

    $Application -> parse()
                 -> run();
}
catch (Exception $e)
{
    echo $e->getMessage();
}

/** --
 * PHP fix
 */

echo "\r\n";
